package com.project.search;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.util.ArrayList;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicLong;

public class Parser {
    // 指定所加载文档的路径(即所下载的离线文档路径).
    // 这里只搜索api相关, 因为平时查的也是api部分, 如果有其他需要改就行.
    private static final String PATH = "/root/save_index_structure/docs/api/";

    // 创建一个 Index 实例, 供 Parser 调用
    private Index index = new Index();

    // 使用标准库提供的 AtomicLong 类来表示时间累计的总和, 初始化都是设置为0
    private AtomicLong total1 = new AtomicLong(0); // 表示解析正文耗费的时间
    private AtomicLong total2 = new AtomicLong(0); // 表示执行 addDoc() 所耗费的时间

    // 通过该方法实现 "单线程制作索引"
    public void run() {
        long begin = System.currentTimeMillis();
        System.out.println("制作索引开始");
        // 整个 Parser 类的入口(方便调试以及其他模块调用)
        // 1. 根据上面指定的路径, 枚举出该路径中所有的文件(html), 该过程需要把所有子目录中的文件都能获取到.
        ArrayList<File> fileList = new ArrayList<>();
        enumFiles(PATH, fileList);
//        System.out.println(fileList);
        // 2. 针对第一点罗列出的 html 文件集, 打开文件, 读取文件内容并进行解析, 最后构建索引.
        for (File file : fileList) {
            // 遍历每个 html 文件, 并进行解析
            System.out.println("开始解析html文件: " + file.getAbsolutePath()); // 方便调试
            parseHTML(file);
        }
        // 3. 把在内存中构建好的索引数据结构 保存到指定文件中
        index.save();
        long end = System.currentTimeMillis();
        System.out.println("制作索引结束, 总消耗时间: " + (end - begin) + "ms");
    }

    // 通过这个方法实现 "多线程制作索引"
    public void runByThread() throws InterruptedException {
        long begin = System.currentTimeMillis();
        System.out.println("开始索引制作操作");

        // 1. 枚举出所有文件
        ArrayList<File> fileList = new ArrayList<>();
        enumFiles(PATH, fileList);

        // 2. 循环遍历文件, 直接引入Java提供的线程池
        CountDownLatch latch = new CountDownLatch(fileList.size());
        ExecutorService executorService = Executors.newFixedThreadPool(7);
        for(File file : fileList) {
            // 将每一个文件都提交到线程池中
            executorService.submit(new Runnable() {
                @Override
                public void run() {
                    // 通过 Runnable 描述任务的具体逻辑
                    //System.out.println("解析HTML文件: " + file.getName());
                    parseHTML(file);
                    latch.countDown();
                }
            });
        }
        // await()阻塞等待, 等待所有线程执行完毕.
        latch.await();
        // 手动的把线程池里面的线程kill掉.
        executorService.shutdown();
        // 3. 保存索引(这里有一点, 如果执行保存索引操作时, 上面的线程还没有执行完毕怎么办?)

        index.save();
        long end = System.currentTimeMillis();
        System.out.println("结束索引制作操作, 消耗总时间: " + (end - begin) + "ms");
        //System.out.println("解析正文耗费时间: " + total1 + "   执行 addDoc() 耗费时间: " + total2);
    }

    private void parseHTML(File file) {
        // 1. 解析出 HTML 的标题.
        String title = parseTitle(file);
        // 2. 解析出 HTML 对应的 url.
        String url = parseUrl(file);
        // 3. 解析出 HTML 的正文
        //long begin = System.nanoTime();
        String content = parseContentByRegex(file);
        //long mid = System.nanoTime();
        // 4. 把解析出来的这些信息插入到索引结构当中.
        index.addDoc(title, url, content);
        //long end = System.nanoTime();
        //total1.addAndGet(mid - begin);
        //total2.addAndGet(end - mid);
    }

    public String parseContent(File file) {
        // 先按照一个字符一个字符的方式来读取
        StringBuilder content = new StringBuilder();
        // try() 执行完毕后, 会自动调用 FileReader.close() 关闭资源方法
        // 手动把缓冲区设为 1M 大小
        try(BufferedReader bufferedReader = new BufferedReader(new FileReader(file), 1024 * 1024)) {
            // 设置一个开关表示是否要进行拷贝 遇到 < 设为 false, 遇到 > 设为 true
            boolean isCopy = true;

            // 设置一个最终保留结果: StringBuilder (这里注意与 StringBuffer 的区别)
            while (true) {
                // 无参数的 read() 方法表示每次读取一个字符
                // 注, 这里的 read() 返回值是 int, 而不是 char
                // 这是因为使用 int 作为返回值是为了表示一些非法值
                // 比如当读取到文件末尾时, 返回 -1 表示文件读取完毕
                int ret = bufferedReader.read();
                if (ret == -1) {
                    // 读取文件完毕跳出循环
                    break;
                }
                // 判断每次读取的字符
                char c = (char) ret;
                if (isCopy) {
                    // 若开关处于打开状态,遇到普通字符就拷贝到 StringBuilder 中
                    if (c == '<') {
                        isCopy = false;
                        continue;
                    }
                    // 其他字符就放到 StringBuilder 中
                    if (c == '\n' || c == '\r') {
                        // 第一次运行时会发现 content 里面有很多换行
                        // 解析正文主要是为了截取描述, 描述里面不应该有换行
                        // 因此添加该 if 语句块将 换行符 替换成 空格
                        c = ' ';
                    }
                    content.append(c);
                } else {
                    // 若开关处于关闭状态, 那么直到遇到 > 才打开
                    if (c == '>') {
                        isCopy = true;
                    }
                }
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
        return content.toString();
    }
    
    public String parseContentByRegex(File file) {
        // 1. 先把整个文件都读到 String 里面
        String content = readFile(file);

        // 2. 替换掉 script 标签
        // 注意String是不可变的 replaceAll() 方法返回的是一个新的String对象.
        // 把新的对象赋值给 content 之前那个被GC自动回收掉.
        content = content.replaceAll("<script.*?>(.*?)</script>", " ");

        // 3. 替换掉普通的 html 标签
        // 注意2,3两点不要搞混顺序, 如果先替换 html 标签, 它会把 script 的标签也替换掉
        // 这样再去替换 script 标签它就找不到了
        content = content.replaceAll("<.*?>", " ");

        // 4. 使用正则把多个空格合并成一个(注意把反斜杠进行转义)
        content = content.replaceAll("\\s+", " ");
        return content;
    }

    private String readFile(File file) {
        // 这里的逻辑和 parseContent() 方法其实是差不多的, 只不过去掉了 "开关"
        try(BufferedReader bufferedReader = new BufferedReader(new FileReader(file))) {
            StringBuilder content = new StringBuilder();
            while (true) {
                int ret = bufferedReader.read();
                if (ret == -1) {
                    break;
                }
                char c = (char) ret;
                if (c == '\n' || c == '\r') {
                    c = ' ';
                }
                content.append(c);
            }
            return content.toString();
        }catch (IOException e) {
            e.printStackTrace();
        }
        // 如果出现其他异常就返回一个空字符串
        return "";
    }

    private String parseUrl(File file) {
        // 获取一个固定前缀路径
        String part1 = "https://docs.oracle.com/javase/8/docs/api/";

        // 截取离线文档路径后半部分
        String part2 = file.getAbsolutePath().substring(PATH.length());
        String part3 = part2.replaceAll("\\\\", "/");
        return part1 + part3;
    }

    private String parseTitle(File file) {
        String fileName = file.getName();
        return fileName.substring(0, fileName.length() - ".html".length());
    }

    private void enumFiles(String path, ArrayList<File> fileList) {
        File rootPath = new File(path);
        // listFiles 获取到了 rootPath 当前目录下的所有子目录(不包含二级目录)
        // 因此要获取子目录里面的内容, 还需要递归
        File[] files = rootPath.listFiles();
        for (File file : files) {
            // 根据当前 file 的类型来决定是否递归.
            // 如果 file 是一个普通文件, 则把 file 加入到 fileList 中.
            // 如果 file 是一个目录, 则递归调用 enumFiles 方法, 进一步获取子目录中的内容
            if (file.isDirectory()) {
                enumFiles(file.getAbsolutePath(), fileList);
            }else {
                // 排除非 html 文件
                if (file.getAbsolutePath().endsWith(".html")) {
                    fileList.add(file);
                }
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        // 通过 main 方法来实现整个制作索引的过程
        Parser parser= new Parser();
        parser.runByThread();
    }
}
